/*
 * SoapUI, Copyright (C) 2004-2017 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent 
 * versions of the EUPL (the "Licence"); 
 * You may not use this work except in compliance with the Licence. 
 * You may obtain a copy of the Licence at: 
 * 
 * http://ec.europa.eu/idabc/eupl 
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is 
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied. See the Licence for the specific language governing permissions and limitations 
 * under the Licence. 
 */

package com.eviware.soapui.impl.wsdl.support.http;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.impl.wsdl.submit.transports.http.support.metrics.SoapUIMetrics;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.http.HttpHost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.ClientConnectionOperator;
import org.apache.http.conn.ClientConnectionRequest;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionPoolTimeoutException;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.conn.ManagedClientConnection;
import org.apache.http.conn.OperatedClientConnection;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SchemeSocketFactory;
import org.apache.http.impl.HttpConnectionMetricsImpl;
import org.apache.http.impl.conn.AbstractPoolEntry;
import org.apache.http.impl.conn.DefaultClientConnection;
import org.apache.http.impl.conn.DefaultClientConnectionOperator;
import org.apache.http.impl.conn.tsccm.BasicPoolEntry;
import org.apache.http.impl.conn.tsccm.BasicPooledConnAdapter;
import org.apache.http.impl.conn.tsccm.PoolEntryRequest;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.io.HttpTransportMetrics;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import org.apache.log4j.Logger;

import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.TimeUnit;

/**
 * Manages a set of HttpConnections for various HostConfigurations. Modified to
 * keep different pools for different keystores.
 */
public class SoapUIMultiThreadedHttpConnectionManager extends ThreadSafeClientConnManager {

    /**
     * Log object for this class.
     */
    private static final Logger log = Logger.getLogger(SoapUIMultiThreadedHttpConnectionManager.class);

    /**
     * Connection eviction policy
     */
    IdleConnectionMonitorThread idleConnectionHandler = new IdleConnectionMonitorThread(this);

    public SoapUIMultiThreadedHttpConnectionManager(SchemeRegistry registry) {
        super(registry);
        idleConnectionHandler.start();
    }

    /**
     * Hook for creating the connection operator. It is called by the
     * constructor. Derived classes can override this method to change the
     * instantiation of the operator. The default implementation here
     * instantiates {@link DefaultClientConnectionOperator
     * DefaultClientConnectionOperator}.
     *
     * @param schreg the scheme registry.
     * @return the connection operator to use
     */
    @Override
    protected ClientConnectionOperator createConnectionOperator(SchemeRegistry schreg) {

        return new SoapUIClientConnectionOperator(schreg);// @ThreadSafe
    }

    public static class IdleConnectionMonitorThread extends Thread {
        private final ClientConnectionManager connMgr;
        private volatile boolean shutdown;

        public IdleConnectionMonitorThread(ClientConnectionManager connMgr) {
            super();
            this.connMgr = connMgr;
        }

        @Override
        public void run() {
            try {
                while (!shutdown) {
                    synchronized (this) {
                        wait(5000);
                        // Close expired connections
                        connMgr.closeExpiredConnections();
                        // Optionally, close connections
                        // that have been idle longer than 30 sec
                        connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                    }
                }
            } catch (InterruptedException ex) {
                // terminate
            }
        }

        public void shutdown() {
            shutdown = true;
            synchronized (this) {
                notifyAll();
            }
        }
    }

    public ClientConnectionRequest requestConnection(final HttpRoute route, final Object state) {

        final PoolEntryRequest poolRequest = pool.requestPoolEntry(route, state);

        return new ClientConnectionRequest() {

            public void abortRequest() {
                poolRequest.abortRequest();
            }

            public ManagedClientConnection getConnection(long timeout, TimeUnit tunit) throws InterruptedException,
                    ConnectionPoolTimeoutException {
                if (route == null) {
                    throw new IllegalArgumentException("Route may not be null.");
                }

                if (log.isDebugEnabled()) {
                    log.debug("Get connection: " + route + ", timeout = " + timeout);
                }

                BasicPoolEntry entry = poolRequest.getPoolEntry(timeout, tunit);
                SoapUIBasicPooledConnAdapter connAdapter = new SoapUIBasicPooledConnAdapter(
                        SoapUIMultiThreadedHttpConnectionManager.this, entry);
                return connAdapter;
            }
        };

    }

    public void releaseConnection(ManagedClientConnection conn, long validDuration, TimeUnit timeUnit) {

        if (!(conn instanceof SoapUIBasicPooledConnAdapter)) {
            throw new IllegalArgumentException("Connection class mismatch, "
                    + "connection not obtained from this manager.");
        }
        SoapUIBasicPooledConnAdapter hca = (SoapUIBasicPooledConnAdapter) conn;
        if ((hca.getPoolEntry() != null) && (hca.getManager() != this)) {
            throw new IllegalArgumentException("Connection not obtained from this manager.");
        }
        synchronized (hca) {
            BasicPoolEntry entry = (BasicPoolEntry) hca.getPoolEntry();
            if (entry == null) {
                return;
            }
            try {
                // make sure that the response has been read completely
                if (hca.isOpen() && !hca.isMarkedReusable()) {
                    // In MTHCM, there would be a call to
                    // SimpleHttpConnectionManager.finishLastResponse(conn);
                    // Consuming the response is handled outside in 4.0.

                    // make sure this connection will not be re-used
                    // Shut down rather than close, we might have gotten here
                    // because of a shutdown trigger.
                    // Shutdown of the adapter also clears the tracked route.
                    hca.shutdown();
                }
            } catch (IOException iox) {
                if (log.isDebugEnabled()) {
                    log.debug("Exception shutting down released connection.", iox);
                }
            } finally {
                boolean reusable = hca.isMarkedReusable();
                if (log.isDebugEnabled()) {
                    if (reusable) {
                        log.debug("Released connection is reusable.");
                    } else {
                        log.debug("Released connection is not reusable.");
                    }
                }
                hca.detach();
                pool.freeEntry(entry, reusable, validDuration, timeUnit);
            }
        }
    }

    @Override
    public void shutdown() {
        super.shutdown(); //To change body of generated methods, choose Tools | Templates.
        idleConnectionHandler.shutdown();
    }


    private class SoapUIClientConnectionOperator extends DefaultClientConnectionOperator {
        public SoapUIClientConnectionOperator(SchemeRegistry schemes) {
            super(schemes);
        }

        @Override
        public OperatedClientConnection createConnection() {
            SoapUIDefaultClientConnection connection = new SoapUIDefaultClientConnection();
            return connection;
        }

        @Override
        public void openConnection(final OperatedClientConnection conn, final HttpHost target, final InetAddress local,
                                   final HttpContext context, final HttpParams params) throws IOException {
            if (conn == null) {
                throw new IllegalArgumentException("Connection may not be null");
            }
            if (target == null) {
                throw new IllegalArgumentException("Target host may not be null");
            }
            if (params == null) {
                throw new IllegalArgumentException("Parameters may not be null");
            }
            if (conn.isOpen()) {
                throw new IllegalStateException("Connection must not be open");
            }

            Scheme schm = schemeRegistry.getScheme(target.getSchemeName());
            SchemeSocketFactory sf = schm.getSchemeSocketFactory();

            //long start = System.nanoTime();
            long start = System.currentTimeMillis();
            InetAddress[] addresses = resolveHostname(target.getHostName());
            // long dnsEnd = System.nanoTime();
            long dnsEnd = System.currentTimeMillis();

            int port = schm.resolvePort(target.getPort());
            for (int i = 0; i < addresses.length; i++) {
                InetAddress address = addresses[i];
                boolean last = i == addresses.length - 1;

                Socket sock = sf.createSocket(params);
                try {
                    // hostname is required by web server with virtual hosts and one IP (TLS-SNI)
                    if (sock instanceof SSLSocket) {
                        PropertyUtils.setProperty(sock, "host", target.getHostName());
                    }
                } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                    SoapUI.logError(e);
                }
                conn.opening(sock, target);

                InetSocketAddress remoteAddress = new InetSocketAddress(address, port);
                InetSocketAddress localAddress = null;
                if (local != null) {
                    localAddress = new InetSocketAddress(local, 0);
                }
                if (log.isDebugEnabled()) {
                    log.debug("Connecting to " + remoteAddress);
                }
                try {
                    Socket connsock = sf.connectSocket(sock, remoteAddress, localAddress, params);
                    if (sock != connsock) {
                        sock = connsock;
                        conn.opening(sock, target);
                    }
                    prepareSocket(sock, context, params);
                    conn.openCompleted(sf.isSecure(sock), params);

                    SoapUIMetrics metrics = (SoapUIMetrics) conn.getMetrics();

                    if (metrics != null) {
                        metrics.getDNSTimer().set(start, dnsEnd);
                    }

                    return;
                } catch (ConnectException ex) {
                    if (last) {
                        throw new HttpHostConnectException(target, ex);
                    }
                } catch (ConnectTimeoutException ex) {
                    if (last) {
                        throw ex;
                    }
                }
                if (log.isDebugEnabled()) {
                    log.debug("Connect to " + remoteAddress + " timed out. "
                            + "Connection will be retried using another IP address");
                }
            }
        }
    }

    private class SoapUIDefaultClientConnection extends DefaultClientConnection {

        public SoapUIDefaultClientConnection() {
            super();
        }

        @Override
        /**
         * @since 4.1
         */
        protected HttpConnectionMetricsImpl createConnectionMetrics(final HttpTransportMetrics inTransportMetric,
                                                                    final HttpTransportMetrics outTransportMetric) {
            return new SoapUIMetrics(inTransportMetric, outTransportMetric);
        }
    }

    static class SoapUIBasicPooledConnAdapter extends BasicPooledConnAdapter {

        protected SoapUIBasicPooledConnAdapter(ThreadSafeClientConnManager tsccm, AbstractPoolEntry entry) {
            super(tsccm, entry);
        }

        @Override
        protected ClientConnectionManager getManager() {
            // override needed only to make method visible in this package
            return super.getManager();
        }

        @Override
        protected AbstractPoolEntry getPoolEntry() {
            // override needed only to make method visible in this package
            return super.getPoolEntry();
        }

        @Override
        protected void detach() {
            // override needed only to make method visible in this package
            super.detach();
        }
    }

}
